/*
 * This file is part of aion-unique <aion-unique.com>.
 *
 *  aion-unique is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  aion-unique is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with aion-unique.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.aionemu.gameserver.skillengine.model;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;

import com.aionemu.commons.utils.Rnd;
import com.aionemu.gameserver.ai.CtrlEvent;
import com.aionemu.gameserver.controllers.movement.ActionObserver;
import com.aionemu.gameserver.controllers.movement.ActionObserver.ObserverType;
import com.aionemu.gameserver.model.TaskId;
import com.aionemu.gameserver.model.gameobjects.Creature;
import com.aionemu.gameserver.model.gameobjects.player.Player;
import com.aionemu.gameserver.model.gameobjects.stats.StatEnum;
import com.aionemu.gameserver.model.templates.item.ItemTemplate;
import com.aionemu.gameserver.network.aion.serverpackets.SM_CASTSPELL;
import com.aionemu.gameserver.network.aion.serverpackets.SM_CASTSPELL_END;
import com.aionemu.gameserver.network.aion.serverpackets.SM_SYSTEM_MESSAGE;
import com.aionemu.gameserver.skillengine.SkillEngine;
import com.aionemu.gameserver.skillengine.action.Action;
import com.aionemu.gameserver.skillengine.action.Actions;
import com.aionemu.gameserver.skillengine.condition.Conditions;
import com.aionemu.gameserver.utils.PacketSendUtility;
import com.aionemu.gameserver.utils.ThreadPoolManager;
import com.aionemu.gameserver.utils.MathUtil;

/**
 * @author ATracer
 */
public class Skill
{
	private List<Creature>		effectedList;

	private Creature			firstTarget;

	private Creature			effector;

	private int					skillLevel;

	private int					skillStackLvl;

	private SkillTemplate		skillTemplate;

	private boolean				firstTargetRangeCheck	= true;

	private ItemTemplate		itemTemplate;

	private int					targetType;

	private boolean				chainSuccess			= true;

	private float				x;
	private float				y;
	private float				z;

	private float				firstTargetRange;
	
	private ActionObserver actionObserver;

	/**
	 * Duration that depends on BOOST_CASTING_TIME
	 */
	private int					duration;
	
	private int 				time;

	public enum SkillType
	{
		CAST, ITEM, PASSIVE
	}

	/**
	 *  Each skill is a separate object upon invocation
	 *  Skill level will be populated from player SkillList
	 *  
	 * @param skillTemplate
	 * @param effector
	 * @param world
	 */
	public Skill(SkillTemplate skillTemplate, Player effector, Creature firstTarget)
	{
		this(skillTemplate, effector, effector.getSkillList().getSkillLevel(skillTemplate.getSkillId()), firstTarget);
	}

	/**
	 * 
	 * @param skillTemplate
	 * @param effector
	 * @param skillLvl
	 * @param firstTarget
	 */
	public Skill(SkillTemplate skillTemplate, Creature effector, int skillLvl, Creature firstTarget)
	{
		this.effectedList = new ArrayList<Creature>();
		this.firstTarget = firstTarget;
		this.skillLevel = skillLvl;
		this.skillStackLvl = skillTemplate.getLvl();
		this.skillTemplate = skillTemplate;
		this.effector = effector;
	}

	/**
	 * Check if the skill can be used
	 * 
	 * @return True if the skill can be used
	 */
	public boolean canUseSkill()
	{
		if (!preCastCheck())
		{
			return false;
		}
		effector.setCasting(this);
		if (effectedList.isEmpty())
		if (!skillTemplate.getTargetProperties().calc(this))
		{
			effector.setCasting(null);
			return false;
		}
		effector.setCasting(null);
		// TODO: Enable non-targeted, non-point AOE skills to trigger. 
		if (targetType == 0 && effectedList.size() == 0)
		{
			return false;
		}
		return true;
	}

	public boolean useSkill()
	{
		return useSkill(false);
	}
	/**
	 *  Skill entry point
	 */
	public boolean useSkill(boolean itemAction)
	{
		if (!canUseSkill())
			return false;
		if (effector.hasTask(TaskId.ACTION))
			return false;

		effector.getObserveController().notifySkilluseObservers(this);

		//start casting
		effector.setCasting(this);

		checkSkillSetException();

		this.duration = effector.getGameStats().getStat(StatEnum.BOOST_CASTING_TIME, skillTemplate.getDuration()).getCurrentInt();
		switch (this.getSkillTemplate().getSubType())
		{
			case ATTACK:
				duration = effector.getGameStats().getStat(StatEnum.BOOST_CASTING_TIME_ATTACK, duration).getCurrentInt();
				break;
			case HEAL:
				duration = effector.getGameStats().getStat(StatEnum.BOOST_CASTING_TIME_HEAL, duration).getCurrentInt();
				break;
			case SUMMON:
				duration = effector.getGameStats().getStat(StatEnum.BOOST_CASTING_TIME_SUMMON, duration).getCurrentInt();
				break;
			case SUMMONTRAP:
				duration = effector.getGameStats().getStat(StatEnum.BOOST_CASTING_TIME_TRAP, duration).getCurrentInt();
				break;
			case SUMMONHOMING:
				duration = effector.getGameStats().getStat(StatEnum.BOOST_CASTING_TIME_SUMMONHOMING, duration).getCurrentInt();
				break;
		}

		int cooldown = skillTemplate.getCooldown();
		if (cooldown != 0)
			effector.setSkillCoolDown(skillTemplate.getSkillId(), cooldown * 100 + this.duration + System.currentTimeMillis());

		if (duration < 0)
			duration = 0;

		if ((skillTemplate.isActive() || skillTemplate.isToggle()) && !itemAction)
		{
			startCast();
		}

		if (this.duration > 0)
		{
			if (!this.isAllowMoved())
			{
				if (effector.getMoveData().moveStartTime > 0)
					effector.onStopMove();
				actionObserver = new ActionObserver(ObserverType.MOVE)
				{
					/* (non-Javadoc)
					 * @see com.aionemu.gameserver.controllers.movement.ActionObserver#moved()
					 */
					@Override
					public void moved()
					{
						effector.abortCast();
					}
				};
				effector.getObserveController().attach(actionObserver);
			}
			effector.addTask(TaskId.ACTION, schedule(this.duration, itemAction));
		}
		else
		{
			endCast(itemAction);
		}
		return true;
	}

	/**
	 * Penalty success skill
	 */
	private void startPenaltySkill()
	{
		if (skillTemplate.getPenaltySkillId() == 0)
			return;

		Skill skill = SkillEngine.getInstance().getSkill(effector, skillTemplate.getPenaltySkillId(), 1, firstTarget);
		skill.useSkill();
	}

	/**
	 *  Start casting of skill
	 */
	private void startCast()
	{
		int targetObjId = firstTarget != null ? firstTarget.getObjectId() : 0;

		switch (targetType)
		{
			case 0: // PlayerObjectId as Target
				PacketSendUtility.broadcastPacketAndReceive(effector, new SM_CASTSPELL(effector.getObjectId(), skillTemplate.getSkillId(), skillLevel,
						targetType, targetObjId, this.duration, time));
				break;

			case 1: // XYZ as Target
				PacketSendUtility.broadcastPacketAndReceive(effector, new SM_CASTSPELL(effector.getObjectId(), skillTemplate.getSkillId(), skillLevel,
						targetType, x, y, z, this.duration, time));
				break;
		}
	}

	/**
	 *  Apply effects and perform actions specified in skill template
	 */
	private void endCast(boolean itemAction)
	{
		if (actionObserver != null)
			effector.getObserveController().removeObserver(actionObserver);
		if (!effector.isCasting())
			return;

		// Check if target is out of skill range
		if (effector instanceof Player && !checkEndCast())
		{
			Player player = (Player) effector;
			player.cancelCurrentSkill();
			PacketSendUtility.sendPacket(player, SM_SYSTEM_MESSAGE.STR_ATTACK_TOO_FAR_FROM_TARGET());
			return;
		}

		//stop casting must be before preUsageCheck()
		effector.setCasting(null);

		if (!preUsageCheck())
			return;

		/**
		 * Create effects and precalculate result
		 */
		int spellStatus = 0;

		final List<Effect> effects = new ArrayList<Effect>();
		if (skillTemplate.getEffects() != null)
		{
			for (Creature effected : effectedList)
			{
				Effect effect = new Effect(effector, effected, skillTemplate, skillLevel, 0, itemTemplate);
				effect.initialize();
				spellStatus = effect.getSpellStatus().getId();
				effects.add(effect);
			}
		}

		// Check Chain Skill Result
		int chainProb = skillTemplate.getChainSkillProb();
		if (chainProb != 0)
		{
			if (Rnd.get(100) < chainProb)
				this.chainSuccess = true;
			else
				this.chainSuccess = false;
		}

		/**
		 * If castspell - send SM_CASTSPELL_END packet
		 */
		if ((skillTemplate.isActive() || skillTemplate.isToggle()) && !itemAction)
		{
			sendCastspellEnd(spellStatus, effects);
		}

		/**
		 * Perform necessary actions (use mp,dp items etc)
		 */
		Actions skillActions = skillTemplate.getActions();
		if (skillActions != null)
		{
			for (Action action : skillActions.getActions())
			{
				action.act(this);
			}
		}

		if (effector.hasAI())
			effector.getAI().notifyEvent(CtrlEvent.EVT_FINISH_CASTING);
		if (time == 0)
		{
			applyEffect(effects);
		}
		else
		{
			ThreadPoolManager.getInstance().schedule(new Runnable()
			{
				public void run()
				{
					applyEffect(effects);
				}
			}, time);
		}
		
	}

	public void applyEffect(List<Effect> effects)
	{
		/**
		 * Apply effects to effected objects
		 */
		for (Effect effect : effects)
		{
			effect.applyEffect();
		}

		/**
		 * Use penalty skill (now 100% success)
		 */
		startPenaltySkill();
	}
	/**
	 * @param spellStatus
	 * @param effects
	 */
	private void sendCastspellEnd(int spellStatus, List<Effect> effects)
	{
		switch (targetType)
		{
			case 0: // PlayerObjectId as Target
				PacketSendUtility.broadcastPacketAndReceive(effector, new SM_CASTSPELL_END(effector, firstTarget, // Need all targets...
						effects, skillTemplate.getSkillId(), skillLevel, skillTemplate.getCooldown(), time, chainSuccess, spellStatus));
				break;

			case 1: // XYZ as Target
				PacketSendUtility.broadcastPacketAndReceive(effector, new SM_CASTSPELL_END(effector, firstTarget, // Need all targets...
						effects, skillTemplate.getSkillId(), skillLevel, skillTemplate.getCooldown(), time, chainSuccess, spellStatus, x, y, z));
				break;
		}
	}

	/**
	 *  Schedule actions/effects of skill (channeled skills)
	 */
	private Future<?> schedule(int delay, final boolean itemAction)
	{
		return ThreadPoolManager.getInstance().schedule(new Runnable()
		{
			public void run()
			{
				effector.cancelTask(TaskId.ACTION);
				effector.getObserveController().removeObserver(actionObserver);
				endCast(itemAction);
			}
		}, delay);
	}

	/**
	 *  Check all conditions before starting cast
	 */
	private boolean preCastCheck()
	{
		Conditions skillConditions = skillTemplate.getStartconditions();
		return checkConditions(skillConditions);
	}

	/**
	 *  Check all conditions before using skill
	 */
	private boolean preUsageCheck()
	{
		Conditions skillConditions = skillTemplate.getUseconditions();
		return checkConditions(skillConditions);
	}

	private boolean checkConditions(Conditions conditions)
	{
		if (conditions != null)
		{
			return conditions.verify(this);
		}
		return true;
	}

	private void checkSkillSetException()
	{
		int setNumber = skillTemplate.getSkillSetException();
		if (effector instanceof Player)
		{
			Player player = (Player) effector;
			if (setNumber != 0)
				player.getEffectController().removeEffectBySetNumber(setNumber);
			else
				player.getEffectController().removeEffectWithSetNumberReserved();
		}
	}

	/**
	 * @return false if target is not in skill range anymore
	 */
	private boolean checkEndCast()
	{
		if (firstTargetRange == 0)
			return true;

		if (effector == firstTarget)
			return true;
		if (!(MathUtil.isIn3dRange(effector, firstTarget, firstTargetRange + 4)))
			return false;
		return true;
	}

	/**
	 * @param value is the firstTargetRange value to set
	 */
	public void setFirstTargetRange(float value)
	{
		firstTargetRange = value;
	}

	/**
	 * @return the effectedList
	 */
	public List<Creature> getEffectedList()
	{
		return effectedList;
	}

	/**
	 * @return the effector
	 */
	public Creature getEffector()
	{
		return effector;
	}

	/**
	 * @return the skillLevel
	 */
	public int getSkillLevel()
	{
		return skillLevel;
	}

	/**
	 * @return the skillStackLvl
	 */
	public int getSkillStackLvl()
	{
		return skillStackLvl;
	}

	/**
	 * @return the skillTemplate
	 */
	public SkillTemplate getSkillTemplate()
	{
		return skillTemplate;
	}

	/**
	 * @return the firstTarget
	 */
	public Creature getFirstTarget()
	{
		return firstTarget;
	}

	/**
	 * @param firstTarget the firstTarget to set
	 */
	public void setFirstTarget(Creature firstTarget)
	{
		this.firstTarget = firstTarget;
	}

	/**
	 * @return true or false
	 */
	public boolean isPassive()
	{
		return skillTemplate.getActivationAttribute() == ActivationAttribute.PASSIVE;
	}

	/**
	 * @return the firstTargetRangeCheck
	 */
	public boolean isFirstTargetRangeCheck()
	{
		return firstTargetRangeCheck;
	}

	/**
	 * @param firstTargetRangeCheck the firstTargetRangeCheck to set
	 */
	public void setFirstTargetRangeCheck(boolean firstTargetRangeCheck)
	{
		this.firstTargetRangeCheck = firstTargetRangeCheck;
	}

	/**
	 * @param itemTemplate the itemTemplate to set
	 */
	public void setItemTemplate(ItemTemplate itemTemplate)
	{
		this.itemTemplate = itemTemplate;
	}

	/**
	 * @param targetType
	 * @param x
	 * @param y
	 * @param z
	 */
	public void setTargetType(int targetType, float x, float y, float z)
	{
		this.targetType = targetType;
		this.x = x;
		this.y = y;
		this.z = z;
	}
	
	private boolean isAllowMoved()
	{
		return getSkillTemplate().isAllowMove();
	}

	/**
	 * @return Returns the time.
	 */
	public int getTime()
	{
		return time;
	}

	/**
	 * @param time The time to set.
	 */
	public void setTime(int time)
	{
		this.time = time;
	}
}
